Skip to content

Optimize cookie parsing#1310

Merged
josevalim merged 2 commits into
elixir-plug:mainfrom
preciz:optimization1
Jun 15, 2026
Merged

Optimize cookie parsing#1310
josevalim merged 2 commits into
elixir-plug:mainfrom
preciz:optimization1

Conversation

@preciz

@preciz preciz commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Use length tracking and binary_part/3 instead of repeated binary concatenation to avoid allocating new binaries during decoding.

Benchmark shows ~2x speedup but it uses slightly more memory.

Bench:

Mix.install([
  {:benchee, "~> 1.0"},
  {:plug, path: "."}
])

defmodule OldCookies do
  def decode(cookie) when is_binary(cookie) do
    Map.new(decode_kv(cookie, []))
  end

  defp decode_kv("", acc), do: acc
  defp decode_kv(<<h, t::binary>>, acc) when h in [?\s, ?\t], do: decode_kv(t, acc)
  defp decode_kv(kv, acc) when is_binary(kv), do: decode_key(kv, "", acc)

  defp decode_key(<<h, t::binary>>, _key, acc) when h in [?\s, ?\t, ?\r, ?\n, ?\v, ?\f],
    do: skip_until_cc(t, acc)

  defp decode_key(<<?;, t::binary>>, _key, acc), do: decode_kv(t, acc)
  defp decode_key(<<?=, t::binary>>, "", acc), do: skip_until_cc(t, acc)
  defp decode_key(<<?=, t::binary>>, key, acc), do: decode_value(t, "", 0, key, acc)
  defp decode_key(<<h, t::binary>>, key, acc), do: decode_key(t, <<key::binary, h>>, acc)
  defp decode_key(<<>>, _key, acc), do: acc

  defp decode_value(<<?;, t::binary>>, value, spaces, key, acc),
    do: decode_kv(t, [{key, trim_spaces(value, spaces)} | acc])

  defp decode_value(<<?\s, t::binary>>, value, spaces, key, acc),
    do: decode_value(t, <<value::binary, ?\s>>, spaces + 1, key, acc)

  defp decode_value(<<h, t::binary>>, _value, _spaces, _key, acc)
       when h in [?\t, ?\r, ?\n, ?\v, ?\f],
       do: skip_until_cc(t, acc)

  defp decode_value(<<h, t::binary>>, value, _spaces, key, acc),
    do: decode_value(t, <<value::binary, h>>, 0, key, acc)

  defp decode_value(<<>>, value, spaces, key, acc),
    do: [{key, trim_spaces(value, spaces)} | acc]

  defp skip_until_cc(<<?;, t::binary>>, acc), do: decode_kv(t, acc)
  defp skip_until_cc(<<_, t::binary>>, acc), do: skip_until_cc(t, acc)
  defp skip_until_cc(<<>>, acc), do: acc

  defp trim_spaces(value, 0), do: value
  defp trim_spaces(value, spaces), do: binary_part(value, 0, byte_size(value) - spaces)
end

defmodule NewCookies do
  def decode(cookie) when is_binary(cookie) do
    Map.new(decode_kv(cookie, []))
  end

  defp decode_kv("", acc), do: acc
  defp decode_kv(<<h, t::binary>>, acc) when h in [?\s, ?\t], do: decode_kv(t, acc)
  defp decode_kv(kv, acc) when is_binary(kv), do: decode_key(kv, kv, 0, acc)

  defp decode_key(<<h, t::binary>>, _key_rest, _len, acc) when h in [?\s, ?\t, ?\r, ?\n, ?\v, ?\f],
    do: skip_until_cc(t, acc)

  defp decode_key(<<?;, t::binary>>, _key_rest, _len, acc), do: decode_kv(t, acc)
  defp decode_key(<<?=, t::binary>>, _key_rest, 0, acc), do: skip_until_cc(t, acc)
  defp decode_key(<<?=, t::binary>>, key_rest, len, acc) do
    key = binary_part(key_rest, 0, len)
    decode_value(t, t, 0, 0, key, acc)
  end
  defp decode_key(<<_, t::binary>>, key_rest, len, acc), do: decode_key(t, key_rest, len + 1, acc)
  defp decode_key(<<>>, _key_rest, _len, acc), do: acc

  defp decode_value(<<?;, t::binary>>, val_rest, len, spaces, key, acc) do
    value = binary_part(val_rest, 0, len - spaces)
    decode_kv(t, [{key, value} | acc])
  end
  defp decode_value(<<?\s, t::binary>>, val_rest, len, spaces, key, acc) do
    decode_value(t, val_rest, len + 1, spaces + 1, key, acc)
  end
  defp decode_value(<<h, t::binary>>, _val_rest, _len, _spaces, _key, acc) when h in [?\t, ?\r, ?\n, ?\v, ?\f],
    do: skip_until_cc(t, acc)
  defp decode_value(<<_, t::binary>>, val_rest, len, _spaces, key, acc) do
    decode_value(t, val_rest, len + 1, 0, key, acc)
  end
  defp decode_value(<<>>, val_rest, len, spaces, key, acc) do
    value = binary_part(val_rest, 0, len - spaces)
    [{key, value} | acc]
  end

  defp skip_until_cc(<<?;, t::binary>>, acc), do: decode_kv(t, acc)
  defp skip_until_cc(<<_, t::binary>>, acc), do: skip_until_cc(t, acc)
  defp skip_until_cc(<<>>, acc), do: acc
end

Benchee.run(
  %{
    "Old Cookie Decoder" => fn input -> OldCookies.decode(input) end,
    "New Cookie Decoder (Optimized)" => fn input -> NewCookies.decode(input) end
  },
  inputs: %{
    "1. Single Cookie" => "session_id=abc123xyz",
    "2. Multi Cookies (Standard)" => "session_id=abc123xyz; user_id=42; theme=dark; logged_in=true",
    "3. Cookies with Whitespaces" => "  foo=bar  ;   baz=qux;other=val   ",
    "4. Malformed/Ignored Pairs" => "valid=one; invalid; valid2=two; =no_key; valid3=three",
    "5. Unicode Values" => "user=barna; location=Budapest; greeting=😊"
  },
  time: 2,
  memory_time: 2
)

Results (noisy system):

    ##### With input 1. Single Cookie #####
    Name                                     ips        average  deviation         median         99th %
    New Cookie Decoder (Optimized)        6.63 M      150.83 ns  ±2126.16%         131 ns         281 ns
    Old Cookie Decoder                    3.03 M      330.24 ns  ±2019.89%         241 ns         481 ns

    Comparison:
    New Cookie Decoder (Optimized)        6.63 M
    Old Cookie Decoder                    3.03 M - 2.19x slower +179.41 ns

    ##### With input 2. Multi Cookies (Standard) #####
    Name                                     ips        average  deviation         median         99th %
    New Cookie Decoder (Optimized)        2.11 M      473.35 ns  ±1176.75%         421 ns         802 ns
    Old Cookie Decoder                    1.00 M      997.58 ns   ±804.64%         741 ns        1753 ns

    Comparison:
    New Cookie Decoder (Optimized)        2.11 M
    Old Cookie Decoder                    1.00 M - 2.11x slower +524.23 ns

    ##### With input 3. Cookies with Whitespaces #####
    Name                                     ips        average  deviation         median         99th %
    New Cookie Decoder (Optimized)        2.97 M      336.31 ns   ±874.27%         311 ns         541 ns
    Old Cookie Decoder                    1.21 M      825.12 ns  ±1121.56%         571 ns        1172 ns

    Comparison:
    New Cookie Decoder (Optimized)        2.97 M
    Old Cookie Decoder                    1.21 M - 2.45x slower +488.81 ns

    ##### With input 4. Malformed/Ignored Pairs #####
    Name                                     ips        average  deviation         median         99th %
    New Cookie Decoder (Optimized)        2.52 M      396.08 ns  ±1085.39%         361 ns         651 ns
    Old Cookie Decoder                    1.03 M      972.69 ns   ±797.30%         632 ns        1452 ns

    Comparison:
    New Cookie Decoder (Optimized)        2.52 M
    Old Cookie Decoder                    1.03 M - 2.46x slower +576.61 ns


    ##### With input 5. Unicode Values #####
    Name                                     ips        average  deviation         median         99th %
    New Cookie Decoder (Optimized)        2.80 M      357.67 ns  ±2403.58%         310 ns         561 ns
    Old Cookie Decoder                    1.34 M      746.02 ns   ±974.55%         541 ns        1132 ns
    Comparison:
    New Cookie Decoder (Optimized)        2.80 M
    Old Cookie Decoder                    1.34 M - 2.09x slower +388.36 ns

preciz added 2 commits June 15, 2026 09:55
Use length tracking and binary_part/3 instead of repeated binary concatenation to avoid allocating new binaries during decoding.
@josevalim josevalim merged commit 746b312 into elixir-plug:main Jun 15, 2026
2 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants